Skip to content

Java 基础--异常内容

Java 异常处理机制

在 Java 开发中,异常处理机制不仅是编程的基础,更是提高系统健壮性与可维护性的关键。


一、Java 异常体系概览

Java 的异常体系以 Throwable 为根基,其核心结构如图所示:

image.png

1.1 Throwable 分类

Throwable 包括两个主要分支:

  • Error 系统级问题,几乎无法通过程序恢复。例如:
    • OutOfMemoryError
    • StackOverflowError
    • NoClassDefFoundError
  • Exception 应用层面的问题,可通过程序逻辑进行处理。它又进一步分为:
    • Checked Exception:必须通过 try-catchthrows 显式处理的异常。
    • Unchecked Exception:也称运行时异常,编译器不会强制要求显式处理。这些通常是代码逻辑错误。

Throwable 类常用方法

  • String getMessage(): 返回异常发生时的详细信息
  • String toString(): 返回异常发生时的简要描述
  • String getLocalizedMessage(): 返回异常对象的本地化信息。使用 Throwable 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 getMessage()返回的结果相同
  • void printStackTrace(): 在控制台上打印 Throwable 对象封装的异常信息

1.2 Checked 与 Unchecked 的对比

特性Checked ExceptionUnchecked Exception
分类Exception(排除 RuntimeExceptionRuntimeException 及其子类
编译检查是,必须显式处理否,编译器不会强制处理
典型场景I/O 操作(IOException)、SQL 错误等NullPointerExceptionArrayIndexOutOfBoundsException
处理方式捕获或抛出(try-catchthrows通常是代码修复,少量情况下捕获处理

RuntimeException 及其子类都统称为非受检查异常,常见的有:

  • NullPointerException(空指针错误)
  • IllegalArgumentException(参数错误比如方法入参类型错误)
  • NumberFormatException(字符串转换为数字格式错误,IllegalArgumentException的子类)
  • ArrayIndexOutOfBoundsException(数组越界错误)
  • ClassCastException(类型转换错误)
  • ArithmeticException(算术错误)
  • SecurityException (安全错误比如权限不够)
  • UnsupportedOperationException(不支持的操作错误比如重复创建同一用户)
  • ……

二、Java 异常处理基础

Java 提供了两种异常处理方式捕获异常抛出异常

2.1 try-catch-finally 机制

  • try:用于捕获可能产生异常的代码。其后可接零个或多个 catch 块,如果没有 catch 块,则必须跟一个 finally 块
  • catch:用于处理捕获的具体异常类型。
  • finally:无论是否捕获到异常,总会执行的代码块,比如资源释放。当在 try 块或 catch 块中遇到 return 语句时,finally 语句块将在方法返回之前被执行。

示例代码与解析:

try {
    System.out.println("Start");
    int result = 10 / 0; // ArithmeticException
} catch (ArithmeticException e) {
    System.out.println("Caught Exception: " + e.getMessage());
} finally {
    System.out.println("Finally Block Executed");
}

输出结果:

Start
Caught Exception: / by zero
Finally Block Executed

注意事项:

  1. finally 中避免使用 return, 否则会覆盖 trycatch 块的返回值。
  2. 如果程序因 系统级错误(如 OutOfMemoryError)导致终止,finally 块可能不会执行。

举例

举例1: **finally 中避免使用 return, 否则会覆盖 trycatch 块的返回值

public int getInt() {
    int i = 0;
    try {
        i = 1;
        return i;
    } finally {
        i = 2;
        return i;
    }
}

public int getInt2() {
    int i = 0;
    try {
        i = 1;
        return i;
    } finally {
        i = 2;
    }
}

Idea插件 jclasslibBytecodeViewer

getInt() 字节码信息

image.png

 0  iconst_0        // 将常量 0 压入操作数栈
 1  istore_1        // 将栈顶的值存储到局部变量表索引 1 中 (i = 0)
 2  iconst_1        // 将常量 1 压入操作数栈
 3  istore_1        // 将栈顶的值存储到局部变量表索引 1 中 (i = 1)
 4  iload_1         // 将局部变量 1 的值加载到操作数栈 (值为 1)
 5  istore_2        // 将栈顶的值存储到局部变量表索引 2 中
 6  iconst_2        // 将常量 2 压入操作数栈
 7  istore_1        // 将栈顶的值存储到局部变量表索引 1 中 (i = 2)
 8  iload_1         // 将局部变量 1 的值加载到操作数栈 (值为 2)
 9  ireturn         // 返回栈顶的值(最终返回的是 2)
10 astore_3         // 异常处理准备:将栈顶的异常对象存储在局部变量表索引 3 中
11 iconst_2         // 将常量 2 压入操作数栈
12 istore_1         // 将栈顶的值存储到局部变量表索引 1 中 (i = 2)
13 iload_1          // 将局部变量 1 的值加载到操作数栈 (值为 2)
14 ireturn          // 返回栈顶的值(在异常处理中最终返回 2)

2.2 try-with-resources 机制

在 Java 7 引入的 try-with-resources 机制,是一种自动管理资源的语法糖。该机制要求资源实现 AutoCloseableCloseable 接口。

try (FileInputStream in = new FileInputStream("test.txt")) {
    int data = in.read();
    System.out.println(data);
} catch (IOException e) {
    e.printStackTrace();
}

优点:

  • 自动释放资源,无需显式调用 close() 方法。
  • 减少因资源未关闭可能引起的内存泄漏。

举例

打包多个文件为 zip 格式

 /**
     * 打包多个文件为 zip 格式
     *
     * @param fileList 文件列表
     */
    public static void zipFile(List<File> fileList) {
        // 文件的压缩包路径
        String zipPath = OUT + "/打包附件.zip";
        // 获取文件压缩包输出流
        try (OutputStream outputStream = new FileOutputStream(zipPath);
             CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
             ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream)) {
            for (File file : fileList) {
                // 获取文件输入流
                InputStream fileIn = new FileInputStream(file);
                // 使用 common.io中的IOUtils获取文件字节数组
                byte[] bytes = IOUtils.toByteArray(fileIn);
                // 写入数据并刷新
                zipOut.putNextEntry(new ZipEntry(file.getName()));
                zipOut.write(bytes, 0, bytes.length);
                zipOut.flush();
            }
        } catch (FileNotFoundException e) {
            System.out.println("文件未找到");
        } catch (IOException e) {
            System.out.println("读取文件异常");
        }
    }

反编译后的代码

    public static void zipFile(List<File> fileList) {
        String zipPath = "./打包附件.zip";

        try {
            OutputStream outputStream = new FileOutputStream(zipPath);
            Throwable var3 = null;

            try {
                CheckedOutputStream checkedOutputStream = new CheckedOutputStream(outputStream, new Adler32());
                Throwable var5 = null;

                try {
                    ZipOutputStream zipOut = new ZipOutputStream(checkedOutputStream);
                    Throwable var7 = null;

                    try {
                        Iterator var8 = fileList.iterator();

                        while(var8.hasNext()) {
                            File file = (File)var8.next();
                            InputStream fileIn = new FileInputStream(file);
                            byte[] bytes = IOUtils.toByteArray(fileIn);
                            zipOut.putNextEntry(new ZipEntry(file.getName()));
                            zipOut.write(bytes, 0, bytes.length);
                            zipOut.flush();
                        }
                    } catch (Throwable var60) {
                        var7 = var60;
                        throw var60;
                    } finally {
                        if (zipOut != null) {
                            if (var7 != null) {
                                try {
                                    zipOut.close();
                                } catch (Throwable var59) {
                                    var7.addSuppressed(var59);
                                }
                            } else {
                                zipOut.close();
                            }
                        }

                    }
                } catch (Throwable var62) {
                    var5 = var62;
                    throw var62;
                } finally {
                    if (checkedOutputStream != null) {
                        if (var5 != null) {
                            try {
                                checkedOutputStream.close();
                            } catch (Throwable var58) {
                                var5.addSuppressed(var58);
                            }
                        } else {
                            checkedOutputStream.close();
                        }
                    }

                }
            } catch (Throwable var64) {
                var3 = var64;
                throw var64;
            } finally {
                if (outputStream != null) {
                    if (var3 != null) {
                        try {
                            outputStream.close();
                        } catch (Throwable var57) {
                            var3.addSuppressed(var57);
                        }
                    } else {
                        outputStream.close();
                    }
                }

            }
        } catch (FileNotFoundException var66) {
            System.out.println("文件未找到");
        } catch (IOException var67) {
            System.out.println("读取文件异常");
        }

    }

从反编辑代码中,可以看到:

try-with-resources 声明是一种简化资源管理的语法,它会在 try 块结束时自动关闭资源,而不需要显式地在 finally 块中调用 close() 方法。在编译时,try-with-resources 会被转换为包含 try-catch-finally 的代码结构,确保资源被正确关闭。


2.3 finally vs try-with-resources:对比表格

特性finallytry-with-resources
资源关闭需要手动在 finally 中完成自动管理(实现 AutoCloseable 接口)
代码可读性较繁琐,需手动检查是否资源为空简洁,无需显式关闭资源
推荐场景复杂操作逻辑,需要确保清理多种资源情况普通 I/O 操作,数据库连接等场景

2.4 抛出异常

在 Java 中,异常机制是用来处理程序运行期间可能出现的错误或意外情况的常见方式。异常的抛出主要通过以下两个关键字实现:


1. throw

  • 定义

    • 用于在方法内部抛出一个具体的异常对象。这是触发异常的实际操作,一旦执行到 throw 语句,当前方法会立即终止,并将异常传递给调用者进行处理或进一步抛出。
  • 核心特点

    • throw 后面必须跟一个具体的异常对象,一般通过 new 操作符来实例化,如:throw new Exception("异常消息")
    • 它直接导致异常的抛出,程序流程从执行到抛出异常点发生中断。

2. throws

  • 定义

    • 用于在方法声明中声明该方法可能抛出的异常类型。它并不会直接抛出异常,而是告诉调用该方法的地方,“请注意,这个方法可能会抛出某种异常,你需要准备妥善处理”。
  • 核心特点

    • throws 只能在方法签名上声明异常类型(一个或者多个异常)。
    • 不会实际抛出任何异常,它只是静态声明。但如果声明的方法在运行时未妥善处理所声明的异常,将会引发编译错误。

throwthrows 的比较

比较维度throwthrows
出现的位置方法的内部(方法体中)方法的声明部分(方法的头部)
作用用于抛出一个具体的异常对象用于声明方法可能会抛出一种或多种异常
功能明确地触发异常告知调用者方法可能引发的异常类型
配合使用必须搭配异常实例(如:throw new Exception())使用通常与方法签名一起出现(如:throws Exception
是否立刻生效当执行到 throw 语句时,立即抛出异常并中断方法执行仅供声明,不会直接触发异常

使用示例:

// 声明方法,表示其可能抛出 IllegalArgumentException 异常
public static void riskyMethod(int value) throws IllegalArgumentException {
    // 检查输入值是否有效
    if (value < 0) {
        // 抛出具体异常
        throw new IllegalArgumentException("值不能为负数!");
    }
    System.out.println("值是:" + value);
}

public static void main(String[] args) {
    try {
        // 调用可能抛出异常的 riskyMethod 方法
        riskyMethod(-5);
    } catch (IllegalArgumentException e) {
        // 捕获并处理由 riskyMethod 抛出的异常
        System.out.println("捕获到异常:" + e.getMessage());
    }
}
  • 分析
    • riskyMethod 方法在声明中使用 throws 关键字说明其可能抛出 IllegalArgumentException 类型的异常。
    • 在方法体内,当 value < 0 时,通过 throw new IllegalArgumentException(...) 抛出具体的异常。
    • main 方法中,调用 riskyMethod 可能会引发异常,因此通过 try-catch 语句捕获并处理异常。

注意事项

检查异常与运行时异常

  • 编译期异常(如 IOException)必须在方法声明中使用 throws,调用它的方法必须显式处理这些异常。
  • 运行时异常(如 NullPointerException)属于非强制处理,哪怕未声明 throws 也不会导致编译报错,程序会在运行过程中可能中断。

异常链

在实际开发中,可以通过 throw 抛出一个新异常,同时将原始异常作为原因嵌套传入,例如:

public class ExceptionChainingDemo {
    public static void main(String[] args) {
        try {
            // 模拟低层方法可能抛出一个异常
            lowerMethod();
        } catch (Exception e) {
            // 在上层捕获异常,并进一步抛出新的异常
            throw new RuntimeException("服务层处理失败", e);
        }
    }

    public static void lowerMethod() throws Exception {
        throw new Exception("底层方法发生错误");
    }
}

三、异常处理规范

阿里巴巴Java异常处理规约

https://www.mapull.com/gitbook/fexa/exception/exception.html

image.png

image.png

image.png

四、不推荐的异常处理方式

1. 捕获阶段

不规范案例:

  1. 不区分异常类型
try {
    // …
} catch (Exception e) { 
    // 不推荐:对所有类型的异常统一处理
    // 没有区分业务异常与系统异常
}

问题:

  • 粗粒度地捕获 Exception 类甚至父类(如 Throwable),导致难以针对不同异常执行细化操作。
  • 会忽略掉系统异常与业务异常的差异,隐藏潜在的严重问题。
  1. 捕获异常不完全

例如:某些特定类型的异常未被捕获,而这些异常可能在运行时发生,程序未提供后续处理机制。


改进建议:

  • 针对 不同的异常类型,逐一列出 catch 块,体现异常处理的意义:
try {
    // 1. 业务逻辑处理
} catch (BusinessException e) { // 捕获业务异常并处理
    log.warn("Business issue: {}", e.getMessage(), e);
} catch (SystemException e) { // 捕获系统异常
    log.error("System issue occurred!", e);
} catch (Exception e) { // 通用异常兜底(可选)
    log.error("Unexpected issue!", e);
    throw new RuntimeException("Critical Error: ", e); // 可能需要重新抛出
}
  • 不要直接捕获顶层异常(如 ThrowableException),除非是特殊情况,如框架中的全局异常处理器。

2. 异常传递阶段

不规范案例:

异常信息丢失

throw new Exception(e.getMessage());

问题:

  • 只保留了异常的消息部分,而丢失了栈踪(StackTrace)信息。这会让开发者难以追踪异常的具体来源。

不必要的异常包装

throw new BIZException(e);

问题:

  • 如果直接包装并不提供新的语义信息,则是冗余的操作,会降低代码清晰度。

异常转译错误

throw new Exception(e);

问题:

  • 将业务异常(高抽象级别)包装为系统异常(低抽象级别),丢失了业务语义信息。

吃掉异常(既不记录日志,也不抛出异常)

catch (Biz4Exception e) {
    // 什么都没处理
}

问题:

  • 在异常发生后没有日志记录,也没有传递到上一层,导致问题静默消失,增加了问题诊断难度。

改进建议:

  • 尽量保留原始异常的上下文信息
try {
    // …
} catch (BusinessException e) {
    throw new NewBusinessException("New context message", e); // 保留原始异常
}
  • 避免不必要的包装: 如果没有带来语义上的重大变化,不要重新包装相同类型的异常:
catch (BIZException e) {
    throw e; // 直接往上传递
}
  • 避免低级抽象异常包装高级抽象异常
catch (BizException e) {
    // 正确做法是传递同一抽象级别异常
    throw new BizException("Detailed business problem", e);
}
  • 避免吃掉异常:

记录日志并通知调用方:

catch (Biz4Exception e) {
    log.error("Biz4Exception occurred", e);
    throw e; // 或者选择重新封装传递
}

3. 异常处理阶段

不规范案例:

  1. 重复处理 同一异常被多个 catch 块或嵌套 try-catch 块重复记录或者处理:
try {
    try {
        // …
    } catch (Biz1Exception e) {
        log.error(e); // 重复 LOG
        throw e;
    }
} catch (BizException e) {
    log.error(e); // 再次记录
    throw e;
}
  1. 处理方式不统一或分散

不同类型异常的处理方式不一致,日志记录、报警、抛出异常等方式各异:

catch (Biz1Exception e) {
    log.warn("Special handling for Biz1", e);
}
catch (Exception e) {
    log.error("General error", e);
    throw e; // 直接抛出
}

改进建议:

  • 减少重复处理: 确保异常只在合理的层次中被捕获并记录,避免重复操作:
try {
    // 核心逻辑
} catch (SpecificException e) {
    log.error("Localized issue", e);
    throw new HighLevelException("Re-wrapped exception", e); // 层次清晰
}
  • 集中处理: 统一的异常处理策略可以避免分散的冗余处理:

  • 提取通用的异常处理逻辑到工具类或拦截器。

  • 封装全局异常捕获机制(如 ControllerAdvice 式处理)。

public class GlobalExceptionHandler {
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<?> handleBusinessException(BusinessException ex) {
        log.warn("Business error: {}", ex.getMessage());
        return ResponseEntity.badRequest().body("Business Error");
    }
}

五、异常处理建议

  • 使用 try-with-resource 关闭资源。
  • 抛出具体的异常而不是 Exception,并在注释中使用 @throw 进行说明。
  • 捕获异常后使用描述性语言记录错误信息,如果是调用外部服务最好是包括入参和出参。
logger.error("说明信息,异常信息:{}", e.getMessage(), e)
  • 优先捕获具体异常。
  • 不要捕获 Throwable 异常,除非特殊情况。
  • 不要忽略异常,异常捕获一定需要处理。
  • 不要同时记录和抛出异常,因为异常会打印多次,正确的处理方式要么抛出异常要么记录异常,如果抛出异常,不要原封不动的抛出,可以自定义异常抛出。
  • 自定义异常不要丢弃原有异常,应该将原始异常传入自定义异常中。
throw MyException("my exception", e);
  • 自定义异常尽量不要使用检查异常。
  • 尽可能晚的捕获异常,如非必要,建议所有的异常都不要在下层捕获,而应该由最上层捕获并统一处理这些异常
  • 避免重复输出异常日志,建议所有的异常日志都统一交由最上层输出。就算下层捕获到了某个异常,如非特殊情况,也不要将异常信息输出,应该交给最上层统一输出日志

六、项目中的异常处理

使用异常的好处:

  • 能够将错误代码和正常代码分离
  • 能够在调用堆栈上传递异常
  • 能够将异常分组和区分

可以通过异常对不同的业务问题进行分类,以便排查问题。

自定义异常

对于 Java 体系中定义的异常类来说,这些是技术层面的异常;而在实际项目中,应用程序中更多是业务方面的异常,比如用户参数输入不合法,用户没有权限等。

可以通过异常对不同的业务问题进行分类,以便排查问题。

抛出自定义异常

if (null == activity) {  
    throw new GenericException(GenericException.Code.NOT_EXISTS, "活动不存在");  
}

通用异常枚举类

package com.shein.plm.philosopherstones.soul.exception;  
  
import com.shein.plm.philosopherstones.soul.enums.ErrorLevel;  
  
public class GenericException extends ExceptionContract {  
    public GenericException(ExceptionCodeContract code) {  
        super(code);  
    }  
  
    public GenericException(ExceptionCodeContract code, String message) {  
        super(code, message);  
    }  
  
    public GenericException(ExceptionCodeContract code, String message, Throwable cause) {  
        super(code, cause);  
    }  
  
    public static enum Code implements ExceptionCodeContract {  
        SUCCESS("0", "成功", ErrorLevel.INFO),  
        FAIL("1", "失败", ErrorLevel.ERROR),  
        ENUM_VALUE_NOT_EXIST("2", "枚举值不存在", ErrorLevel.ERROR),  
        UNAUTHENTICATED("302", "未登录", ErrorLevel.INFO),  
        PARAMS_ERROR("400", "请求参数错误", ErrorLevel.INFO),  
        UNAUTHORIZED("403", "无权限", ErrorLevel.INFO),  
        ROUTE_NOT_FOUND("404", "路由不存在", ErrorLevel.WARN),  
        ILLEGAL_OPERATION("405", "非法操作", ErrorLevel.WARN),  
        REPEAT_OPERATION("406", "重复操作", ErrorLevel.WARN),  
        SYSTEM_MAINTAINING("500", "系统维护升级中,请稍后再试~", ErrorLevel.ERROR),  
        SYSTEM_ERROR("501", "系统出现异常,请联系客服号处理", ErrorLevel.ERROR),  
        RPC_ERROR("502", "请求 【{}】 外部系统异常,{}", ErrorLevel.ERROR),  
        ALREADY_EXISTS("600", "数据已存在,不可重复添加", ErrorLevel.ERROR),  
        NOT_EXISTS("601", "数据不存在", ErrorLevel.ERROR),  
        CASTS_NOT_DEFINED("602", "模型字段未定义", ErrorLevel.ERROR),  
        DATA_OUTED("603", "数据已过期", ErrorLevel.ERROR),  
        INSERT_FAIL("604", "数据插入失败", ErrorLevel.ERROR),  
        SEARCH_RANGE_TOO_LARGE("605", "搜索范围过大,请调整搜索条件来缩小搜索范围!", ErrorLevel.ERROR),  
        SEARCH_FIELD_NOT_DEFINED("606", "搜索字段未定义", ErrorLevel.ERROR),  
        SEARCH_FAIL("607", "搜索失败", ErrorLevel.ERROR),  
        SEARCH_TIMEOUT("608", "搜索范围过大导致超时,请检查模糊搜索输入框是否可以输入更多的信息来缩小搜索范围,或者清空模糊搜索输入框内容!", ErrorLevel.ERROR);  
  
        private String code;  
        private String description;  
        private ErrorLevel level;  
  
        public String getCode() {  
            return this.code;  
        }  
  
        public String getDescription() {  
            return this.description;  
        }  
  
        public ErrorLevel getLevel() {  
            return this.level;  
        }  
  
        private Code(String code, String description, ErrorLevel level) {  
            this.code = code;  
            this.description = description;  
            this.level = level;  
        }  
    }  
}

业务异常和系统异常

该异常用户能否处理,如果用户能处理则抛出业务异常,如果用户不能处理需要程序员处理则抛出系统异常。

业务异常:比如:“用户没有登录”,“没有权限操作”。

系统异常:如 NullPointerException,IndexOfException。

image.png

错误码

异常码表: https://wiki.dotfashion.cn/pages/viewpage.action?pageId=356122791

如何处理异常

尽可能晚的捕获异常,如非必要,建议所有的异常都不要在下层捕获,而应该由最上层捕获并统一处理这些异常

全局异常处理

参考: